#!/usr/bin/env python
#Copyright (c) 2011 Isaac Dawson (WBTS Project)
#Permission is hereby granted, free of charge, to any person obtaining a copy 
#of this software and associated documentation files (the "Software"), to deal 
#in the Software without restriction, including without limitation the rights 
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
#copies of the Software, and to permit persons to whom the Software is furnished
#to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
#THE SOFTWARE.
import sys
import os
import re
import ConfigParser

from twisted.web import static, script, twcgi

# validation functions
from wbts.utils import mgmt_utils
# The following imports are used in resolution of our processors
# for custom processors, please put them in the wbts.web
# path/package.
from wbts.web.processors import *


REQD = 0
NOT_REQD = 1
SETTING_TYPE = 1
CONFIG_SECTIONS = {
    'SSLSettings': {
        'ssl_cert': [REQD, 'file'],
        'ssl_port': [REQD, 'port']
    },
    'DNSSettings': {
        'named_root': [REQD, 'path'],
        'alt_server': [NOT_REQD, 'host'],
        'alt_port': [NOT_REQD, 'port']
    },                          
    'ManagementSettings': {
        'mgmt_root': [REQD, 'path'],
        'mgmt_port': [REQD, 'port']
    },  
    'VirtualHostProcessors': {
        '.rpy': [REQD, 'processor'],
        '.cgi': [NOT_REQD, 'processor'],
        '.asis': [NOT_REQD, 'processor']
    },
    'VirtualHostMappings': {
        'default': [REQD, 'path'],
        'attacker.com': [REQD, 'path'],
        'www.attacker.com': [REQD, 'path'],
        'attacker.victim.com': [REQD, 'path'],
        'victim.com': [REQD, 'path'],
        'www.victim.com': [REQD, 'path'],
        'sub.victim.com': [REQD, 'path'],
    },                          
    'WebSettings': {
        'http_port': [REQD, 'port'],
        'shared_root': [REQD, 'path']
    },
    'DBClientSettings': {
        'db_class': [REQD, 'class'],
        'db_name': [REQD, 'db_name'],
        'db_connectstring': [REQD, 'db_connectstring']
    },
    'PrivilegeSettings': {
        'userid': [REQD, 'id'],
        'groupid': [REQD, 'id']
    }
}

class WBTSConfig():
    mFilename = "wbts.cfg"
    mConfigFP = None
    mConfig = ConfigParser.ConfigParser()
    
    def loadCfg(self):
        self._openCfg('r')
        self.mConfig.readfp(self.mConfigFP)
        self._checkLoadedCfg()
        return
    
    def openCfg(self):
        self._openCfg('w')
    
    def saveCfg(self):
        self.mConfig.write(self.mConfigFP)
        
    def _openCfg(self, mode):
        try:
            self.mConfigFP = open(self.mFilename, mode)
        except IOError, msg:
            print "Unable to open %s."%self.mFilename
            print "Maybe you should run: python create_config.py?"
            exit(1)
    
    def _checkLoadedCfg(self):
        for section,section_values in CONFIG_SECTIONS.items():
            for config_name, config_metainfo in section_values.items():
                if (self.mConfig.has_section(section) and
                    self.mConfig.has_option(section, config_name)):
                    
                    config_value = self.mConfig.get(section, config_name)
                    ret = self._checkValue(config_name,
                                           config_value,
                                           config_metainfo[SETTING_TYPE])
                    if (ret == False):
                        print "Error %s has an invalid value of %s"\
                        %(config_name, config_value)
                        exit(1)
        return
        
    def getCfgValue(self, section, name):
        """
        Returns the configuration value from our loaded configuration.
        """
        if (self.mConfig.has_section(section) and
            self.mConfig.has_option(section, name) ):
            try:
                config_type = CONFIG_SECTIONS[section][name][SETTING_TYPE]
                if (config_type == 'port' or config_type == 'id'):
                    return self.mConfig.getint(section, name)
            except ValueError, msg:
                print "Error getting configuration value: %s"%(msg,)
                return None
            # for windows file paths we need to ensure backslashes do not become
            # escapes, had issues with c:\dir\release (note \r is an escape).
            return self.mConfig.get(section, name).replace('\\', '\\\\') 
        return None
    
    def getCfgSectionValues(self, section):
        """
        Returns all of the configuration values for a specific section in a
        dictionary object. We call getCfgValue to ensure we return the proper
        value type.
        """
        if (self.mConfig.has_section(section)):
            # unfortunately config.items(section) return a list of tuples...
            # this way we can validate and get the proper type (int for ports)
            section_values = {}
            for value in self.mConfig.options(section):
                section_values[value] = self.getCfgValue(section, value)
            return section_values
    
        return None
    
    def addCfgValue(self, section, name, value, value_type=None,
                    validator_callable=None):

        ret = self._checkValue(name, value, value_type, validator_callable)
        if (ret == False):
            print "Error %s is an invalid %s! "%(name, value_type)
            return False
        
        if (not self.mConfig.has_section(section)):
            self.mConfig.add_section(section)

        self.mConfig.set(section, name, value)
        return True
    
    def checkValue(self, value, value_type):
        """
        Checks if a value of value_type is valid or not.
        Just calls _checkValue internally.
        """
        return self._checkValue('dummy', value, value_type, None)
        
    def _checkValue(self, name, value, value_type, validator_callable=None):
        """
        Checks the configuration value is valid. You can alternatively pass in
        a callable if you need to do some additional/different validation of it.
        Keep in mind the validation callable must return True/False!
        @returns
        True for valid
        False for invalid.
        """
        ret = False
        if (validator_callable != None):
            return validator_callable(value)
        
        if (value_type == "port"):
            ret = self.isPortValid(value)
        elif (value_type == "host"):
            ret = self.isHostValid(value)
        elif (value_type == "path"):
            ret = self.isPathValid(value)
        elif (value_type == "file"):
            ret = self.isFileValid(value)
        elif (value_type == "id"):
            ret = self.isIdValid(value)
        elif (value_type == "processor"):
            ret = self.isProcessorValid(name, value)
        elif (value_type == "class"):
            ret = self.isClassValid(value)
        elif (value_type == "db_connectstring" or value_type == "db_name"):
            ret = True
        elif (value_type == ""):
            ret = True
        return ret
    
    def delCfgValue(self, section, name):
        """
        deletes the configuration value.
        @returns
        False on could not remove
        0 on successfully removed.
        """
        ret = False
        if (self.mConfig.has_section(section) and
            self.mConfig.has_option(name)):
            self.mConfig.remove_option(section, name)
            ret = True
        return ret
    
    ### VALIDATION RELATED METHODS ###
    def isClassValid(self, string_value):
        return mgmt_utils.is_class_valid(string_value)
        
    def isIdValid(self, id_value):
        return mgmt_utils.is_id_valid(id_value)
        
    def isPortValid(self, port_value):
        return mgmt_utils.is_port_valid(port_value)
    
    def isFileValid(self, file_value):
        return mgmt_utils.is_file_valid(file_value)
    
    def isPathValid(self, path_value):
        return mgmt_utils.is_path_valid(path_value)
        
    def isHostValid(self, host_value):
        """
        TODO: IMPLEMENT
        """
        return True

    def isProcessorValid(self, extension, processor):
        if (extension.startswith('.')):
            extension = extension[1:]
        m = re.search('^[a-zA-Z0-9\-~_$]*$', extension)
        if (m == None):
            log.err("Supplied extension [%s] is not valid."%extension)
            return False
        # some malicious stuff can probably slip by this, but g'enuff.
        m = re.search('^[a-zA-Z0-9\-,.:;/\\\ ()]*$', processor)
        if (m == None):
            log.err("Supplied processor [%s] is not valid."%processor)
            return False
        
        ret = self._validateProcessor(processor)
        if (ret == None):
            return False

        return True
    
    def _validateProcessor(self, processor_string):
        """
    validate_processor - Looks up the extension's processor
    from either twisted.web.(static|twcgi|script) or our own
    wbts.web.processors package. The string is simply eval'd
    and an instance is put in our options dict.
        """
        try:
            
            if (',' in processor_string):
                processor_tuple = processor_string.split(',')
                if (len(processor_tuple) == 2):
                    processor,interpreter = processor_tuple
                    if (not os.path.exists(interpreter)):
                        print "Error could not find: %s"%interpreter
                        print "Please check the wbts.cfg file"
                        exit()
                    else:
                        resolved_proc = eval(processor)
                        resolved_proc.filter = interpreter
            else:
                resolved_proc = eval(processor_string)
        except NameError, msg:
            print "Unable to resolve processor: %s"%processor_tuple[0]
            print "Reason: %s"%msg
            resolved_proc = None
        except AttributeError, msg:
            print "Unable to resolve processor %s from our module."\
            %processor_tuple[0]
            print "Reason: %s"%msg
            resolved_proc = None
        return resolved_proc

    def resolveProcessors(self, setting):
        """
    resolve_processors - Loop over each processor/extension and
    call validate_processor we fail hard if a processor can not
    be resolved.
        """
        options = {}
        resolved_proc = None
        for ext, proc in setting.items():
            #processor_tuple = proc.split(',')
            resolved_proc = self._validateProcessor(proc)
            if (resolved_proc == None):
                exit()
            # Make sure we start with a .
            if ( ext.startswith('.') == False):
                ext = "." + ext
            options[ext] = resolved_proc
        return options
